build_env.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. """Build Environment used for isolation during sdist building
  2. """
  3. import logging
  4. import os
  5. import sys
  6. import textwrap
  7. from collections import OrderedDict
  8. from distutils.sysconfig import get_python_lib
  9. from sysconfig import get_paths
  10. from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet
  11. from pip import __file__ as pip_location
  12. from pip._internal.cli.spinners import open_spinner
  13. from pip._internal.utils.subprocess import call_subprocess
  14. from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
  15. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  16. if MYPY_CHECK_RUNNING:
  17. from types import TracebackType
  18. from typing import Tuple, Set, Iterable, Optional, List, Type
  19. from pip._internal.index.package_finder import PackageFinder
  20. logger = logging.getLogger(__name__)
  21. class _Prefix:
  22. def __init__(self, path):
  23. # type: (str) -> None
  24. self.path = path
  25. self.setup = False
  26. self.bin_dir = get_paths(
  27. 'nt' if os.name == 'nt' else 'posix_prefix',
  28. vars={'base': path, 'platbase': path}
  29. )['scripts']
  30. # Note: prefer distutils' sysconfig to get the
  31. # library paths so PyPy is correctly supported.
  32. purelib = get_python_lib(plat_specific=False, prefix=path)
  33. platlib = get_python_lib(plat_specific=True, prefix=path)
  34. if purelib == platlib:
  35. self.lib_dirs = [purelib]
  36. else:
  37. self.lib_dirs = [purelib, platlib]
  38. class BuildEnvironment(object):
  39. """Creates and manages an isolated environment to install build deps
  40. """
  41. def __init__(self):
  42. # type: () -> None
  43. temp_dir = TempDirectory(
  44. kind=tempdir_kinds.BUILD_ENV, globally_managed=True
  45. )
  46. self._prefixes = OrderedDict((
  47. (name, _Prefix(os.path.join(temp_dir.path, name)))
  48. for name in ('normal', 'overlay')
  49. ))
  50. self._bin_dirs = [] # type: List[str]
  51. self._lib_dirs = [] # type: List[str]
  52. for prefix in reversed(list(self._prefixes.values())):
  53. self._bin_dirs.append(prefix.bin_dir)
  54. self._lib_dirs.extend(prefix.lib_dirs)
  55. # Customize site to:
  56. # - ensure .pth files are honored
  57. # - prevent access to system site packages
  58. system_sites = {
  59. os.path.normcase(site) for site in (
  60. get_python_lib(plat_specific=False),
  61. get_python_lib(plat_specific=True),
  62. )
  63. }
  64. self._site_dir = os.path.join(temp_dir.path, 'site')
  65. if not os.path.exists(self._site_dir):
  66. os.mkdir(self._site_dir)
  67. with open(os.path.join(self._site_dir, 'sitecustomize.py'), 'w') as fp:
  68. fp.write(textwrap.dedent(
  69. '''
  70. import os, site, sys
  71. # First, drop system-sites related paths.
  72. original_sys_path = sys.path[:]
  73. known_paths = set()
  74. for path in {system_sites!r}:
  75. site.addsitedir(path, known_paths=known_paths)
  76. system_paths = set(
  77. os.path.normcase(path)
  78. for path in sys.path[len(original_sys_path):]
  79. )
  80. original_sys_path = [
  81. path for path in original_sys_path
  82. if os.path.normcase(path) not in system_paths
  83. ]
  84. sys.path = original_sys_path
  85. # Second, add lib directories.
  86. # ensuring .pth file are processed.
  87. for path in {lib_dirs!r}:
  88. assert not path in sys.path
  89. site.addsitedir(path)
  90. '''
  91. ).format(system_sites=system_sites, lib_dirs=self._lib_dirs))
  92. def __enter__(self):
  93. # type: () -> None
  94. self._save_env = {
  95. name: os.environ.get(name, None)
  96. for name in ('PATH', 'PYTHONNOUSERSITE', 'PYTHONPATH')
  97. }
  98. path = self._bin_dirs[:]
  99. old_path = self._save_env['PATH']
  100. if old_path:
  101. path.extend(old_path.split(os.pathsep))
  102. pythonpath = [self._site_dir]
  103. os.environ.update({
  104. 'PATH': os.pathsep.join(path),
  105. 'PYTHONNOUSERSITE': '1',
  106. 'PYTHONPATH': os.pathsep.join(pythonpath),
  107. })
  108. def __exit__(
  109. self,
  110. exc_type, # type: Optional[Type[BaseException]]
  111. exc_val, # type: Optional[BaseException]
  112. exc_tb # type: Optional[TracebackType]
  113. ):
  114. # type: (...) -> None
  115. for varname, old_value in self._save_env.items():
  116. if old_value is None:
  117. os.environ.pop(varname, None)
  118. else:
  119. os.environ[varname] = old_value
  120. def check_requirements(self, reqs):
  121. # type: (Iterable[str]) -> Tuple[Set[Tuple[str, str]], Set[str]]
  122. """Return 2 sets:
  123. - conflicting requirements: set of (installed, wanted) reqs tuples
  124. - missing requirements: set of reqs
  125. """
  126. missing = set()
  127. conflicting = set()
  128. if reqs:
  129. ws = WorkingSet(self._lib_dirs)
  130. for req in reqs:
  131. try:
  132. if ws.find(Requirement.parse(req)) is None:
  133. missing.add(req)
  134. except VersionConflict as e:
  135. conflicting.add((str(e.args[0].as_requirement()),
  136. str(e.args[1])))
  137. return conflicting, missing
  138. def install_requirements(
  139. self,
  140. finder, # type: PackageFinder
  141. requirements, # type: Iterable[str]
  142. prefix_as_string, # type: str
  143. message # type: str
  144. ):
  145. # type: (...) -> None
  146. prefix = self._prefixes[prefix_as_string]
  147. assert not prefix.setup
  148. prefix.setup = True
  149. if not requirements:
  150. return
  151. args = [
  152. sys.executable, os.path.dirname(pip_location), 'install',
  153. '--ignore-installed', '--no-user', '--prefix', prefix.path,
  154. '--no-warn-script-location',
  155. ] # type: List[str]
  156. if logger.getEffectiveLevel() <= logging.DEBUG:
  157. args.append('-v')
  158. for format_control in ('no_binary', 'only_binary'):
  159. formats = getattr(finder.format_control, format_control)
  160. args.extend(('--' + format_control.replace('_', '-'),
  161. ','.join(sorted(formats or {':none:'}))))
  162. index_urls = finder.index_urls
  163. if index_urls:
  164. args.extend(['-i', index_urls[0]])
  165. for extra_index in index_urls[1:]:
  166. args.extend(['--extra-index-url', extra_index])
  167. else:
  168. args.append('--no-index')
  169. for link in finder.find_links:
  170. args.extend(['--find-links', link])
  171. for host in finder.trusted_hosts:
  172. args.extend(['--trusted-host', host])
  173. if finder.allow_all_prereleases:
  174. args.append('--pre')
  175. if finder.prefer_binary:
  176. args.append('--prefer-binary')
  177. args.append('--')
  178. args.extend(requirements)
  179. with open_spinner(message) as spinner:
  180. call_subprocess(args, spinner=spinner)
  181. class NoOpBuildEnvironment(BuildEnvironment):
  182. """A no-op drop-in replacement for BuildEnvironment
  183. """
  184. def __init__(self):
  185. # type: () -> None
  186. pass
  187. def __enter__(self):
  188. # type: () -> None
  189. pass
  190. def __exit__(
  191. self,
  192. exc_type, # type: Optional[Type[BaseException]]
  193. exc_val, # type: Optional[BaseException]
  194. exc_tb # type: Optional[TracebackType]
  195. ):
  196. # type: (...) -> None
  197. pass
  198. def cleanup(self):
  199. # type: () -> None
  200. pass
  201. def install_requirements(
  202. self,
  203. finder, # type: PackageFinder
  204. requirements, # type: Iterable[str]
  205. prefix_as_string, # type: str
  206. message # type: str
  207. ):
  208. # type: (...) -> None
  209. raise NotImplementedError()